Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 97170627fea83b3cb4b485ed958c74745ea17c9f


Parents : dcfd93c
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-03T20:36:02-05:00

feat(micron-wasm): implement runtime override for Micron-Parser-Go WASM with GitHub release support and UI modal for updates

Changes
Diff

diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 734cb199..01d23cee 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -3977,6 +3977,77 @@ class ReticulumMeshChat:
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
+ @routes.get("/api/v1/tools/micron-parser-go-release")
+ async def tools_micron_parser_go_release(request):
+ """Proxy Micron-Parser-Go release files from one fixed GitHub repo (CSP-safe).
+
+ Browsers cannot fetch github.com under the default connect-src; the server
+ fetches only https://github.com/Quad4-Software/Micron-Parser-Go/releases/download/{tag}/{asset}.
+ """
+ tag = (request.query.get("tag") or "").strip()
+ asset = (request.query.get("asset") or "").strip()
+ allowed_assets = frozenset({"SHASUMS256.txt", "micron-parser-go.wasm"})
+ if asset not in allowed_assets:
+ return web.json_response({"error": "Invalid asset"}, status=400)
+ if not tag or len(tag) > 128:
+ return web.json_response({"error": "Invalid tag"}, status=400)
+ if any(
+ c in tag for c in ("/", "\\", "..", "?", "#", " ", "\t", "\n", "\r")
+ ):
+ return web.json_response({"error": "Invalid tag"}, status=400)
+ if not re.fullmatch(r"v[A-Za-z0-9._-]+", tag):
+ return web.json_response({"error": "Invalid tag format"}, status=400)
+
+ upstream = (
+ "https://github.com/Quad4-Software/Micron-Parser-Go/releases/download/"
+ f"{tag}/{asset}"
+ )
+ if asset == "SHASUMS256.txt":
+ max_bytes = 64 * 1024
+ else:
+ max_bytes = 20 * 1024 * 1024
+
+ headers_out = {
+ "Cache-Control": "no-store",
+ }
+ gh_headers = {"User-Agent": "MeshChatX-MicronWasmRelease/1.0"}
+
+ try:
+ timeout = aiohttp.ClientTimeout(total=120)
+ async with aiohttp.ClientSession(timeout=timeout) as session:
+ async with session.get(
+ upstream, headers=gh_headers, allow_redirects=True
+ ) as response:
+ if response.status != 200:
+ return web.json_response(
+ {
+ "error": (
+ f"Upstream returned {response.status} "
+ f"for Micron-Parser-Go {tag}/{asset}"
+ )
+ },
+ status=502,
+ )
+ data = await response.read()
+ if len(data) > max_bytes:
+ return web.json_response(
+ {"error": "Release asset exceeds size limit"},
+ status=502,
+ )
+ if asset == "SHASUMS256.txt":
+ return web.Response(
+ body=data,
+ content_type="text/plain",
+ headers=headers_out,
+ )
+ return web.Response(
+ body=data,
+ content_type="application/wasm",
+ headers=headers_out,
+ )
+ except Exception as e:
+ return web.json_response({"error": str(e)}, status=502)
+
# fetch reticulum interfaces
@routes.get("/api/v1/reticulum/interfaces")
async def reticulum_interfaces(request):

diff --git a/meshchatx/src/frontend/components/settings/MicronWasmUpdateModal.vue b/meshchatx/src/frontend/components/settings/MicronWasmUpdateModal.vue
new file mode 100644
index 00000000..206fc058
--- /dev/null
+++ b/meshchatx/src/frontend/components/settings/MicronWasmUpdateModal.vue
@@ -0,0 +1,292 @@
+<!-- SPDX-License-Identifier: 0BSD -->
+
+<template>
+ <v-dialog
+ :model-value="modelValue"
+ max-width="560"
+ scrollable
+ @update:model-value="$emit('update:modelValue', $event)"
+ >
+ <v-card class="bg-white dark:bg-zinc-900">
+ <v-card-title class="text-h6 font-semibold text-gray-900 dark:text-white border-b dark:border-zinc-800">
+ {{ $t("settings.micron_wasm_update_modal_title") }}
+ </v-card-title>
+ <v-card-text class="pt-4 space-y-4 text-gray-800 dark:text-zinc-200">
+ <div
+ v-if="currentInfo"
+ class="rounded-lg border border-gray-200 dark:border-zinc-700 p-3 text-sm space-y-1"
+ >
+ <div class="font-medium">{{ $t("settings.micron_wasm_update_active_label") }}</div>
+ <div>
+ {{
+ $t("settings.micron_wasm_update_active_source", { source: sourceLabel(currentInfo.source) })
+ }}
+ </div>
+ <div class="monospace-field break-all text-xs opacity-90">{{ currentInfo.releaseTag }}</div>
+ <div v-if="currentInfo.byteLength" class="text-xs text-gray-600 dark:text-zinc-400">
+ {{ $t("settings.micron_wasm_update_active_size", { bytes: currentInfo.byteLength }) }}
+ </div>
+ </div>
+ <div v-else class="text-sm text-gray-600 dark:text-zinc-400">
+ {{ $t("settings.micron_wasm_update_bundled_only") }}
+ </div>
+
+ <p class="text-sm text-gray-600 dark:text-zinc-400">
+ {{ $t("settings.micron_wasm_update_isolation_note") }}
+ </p>
+
+ <div class="space-y-2">
+ <div class="text-sm font-medium">{{ $t("settings.micron_wasm_update_github_heading") }}</div>
+ <div class="text-xs text-gray-600 dark:text-zinc-400 break-all">
+ {{ $t("settings.micron_wasm_update_github_url_hint") }}<br />
+ {{ downloadBase }}/{{ releaseTagInput.trim() || defaultTag }}/{{ wasmFileName }}
+ </div>
+ <a
+ class="text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline underline-offset-2"
+ href="https://github.com/Quad4-Software/Micron-Parser-Go/releases"
+ target="_blank"
+ rel="noopener noreferrer"
+ >https://github.com/Quad4-Software/Micron-Parser-Go/releases</a
+ >
+ <div class="flex flex-wrap items-end gap-2">
+ <input
+ v-model="releaseTagInput"
+ type="text"
+ class="input-field flex-1 min-w-[8rem] monospace-field"
+ :placeholder="defaultTag"
+ :disabled="busy"
+ autocomplete="off"
+ spellcheck="false"
+ />
+ <v-btn
+ color="primary"
+ variant="flat"
+ :loading="busy"
+ :disabled="busy"
+ @click="onFetchFromGitHub"
+ >
+ {{ $t("settings.micron_wasm_update_fetch_github") }}
+ </v-btn>
+ </div>
+ </div>
+
+ <div class="space-y-2">
+ <div class="text-sm font-medium">{{ $t("settings.micron_wasm_update_upload_heading") }}</div>
+ <p class="text-xs text-amber-800 dark:text-amber-200/90">
+ {{ $t("settings.micron_wasm_update_upload_warning") }}
+ </p>
+ <input
+ ref="fileInput"
+ type="file"
+ accept=".wasm,application/wasm"
+ class="text-sm max-w-full"
+ :disabled="busy"
+ @change="onWasmFileSelected"
+ />
+ </div>
+
+ <v-alert v-if="formError" type="error" density="compact" variant="tonal" class="text-sm">
+ {{ formError }}
+ </v-alert>
+
+ <div class="flex flex-wrap gap-2 pt-2 items-center">
+ <v-btn variant="text" color="secondary" :disabled="busy || !currentInfo" @click="onRevertBundled">
+ {{ $t("settings.micron_wasm_update_revert_bundled") }}
+ </v-btn>
+ <div class="flex-1" />
+ <v-btn variant="text" @click="close">{{ $t("common.close") }}</v-btn>
+ </div>
+ </v-card-text>
+ </v-card>
+ </v-dialog>
+</template>
+
+<script>
+import ToastUtils from "../../js/ToastUtils";
+import {
+ MICRON_PARSER_GO_RELEASE_DOWNLOAD_BASE,
+ WASM_FILENAME,
+ fetchWasmFromGitHubReleaseVerified,
+ getMicronWasmRuntimeOverride,
+ setMicronWasmRuntimeOverride,
+ clearMicronWasmRuntimeOverride,
+ computeWasmSriSha384,
+ MAX_WASM_OVERRIDE_BYTES,
+} from "../../js/MicronWasmRuntimeOverride.js";
+import {
+ invalidateNomadMicronWasmPreload,
+ refreshMicronWasmRuntimeOverrideCache,
+ preloadNomadMicronWasm,
+} from "../../js/MicronWasmLoader.js";
+
+export default {
+ name: "MicronWasmUpdateModal",
+ props: {
+ modelValue: { type: Boolean, default: false },
+ },
+ emits: ["update:modelValue", "saved"],
+ data() {
+ return {
+ busy: false,
+ formError: "",
+ currentInfo: null,
+ releaseTagInput: "",
+ abort: null,
+ };
+ },
+ computed: {
+ defaultTag() {
+ const t = import.meta.env.VITE_MICRON_PARSER_GO_RELEASE;
+ return typeof t === "string" && t.trim() ? t.trim() : "v1.0.5";
+ },
+ downloadBase() {
+ return MICRON_PARSER_GO_RELEASE_DOWNLOAD_BASE;
+ },
+ wasmFileName() {
+ return WASM_FILENAME;
+ },
+ },
+ watch: {
+ modelValue(v) {
+ if (v) {
+ this.formError = "";
+ this.releaseTagInput = this.defaultTag;
+ this.loadCurrentInfo();
+ } else if (this.abort) {
+ this.abort.abort();
+ this.abort = null;
+ }
+ },
+ },
+ methods: {
+ sourceLabel(source) {
+ if (source === "upload") {
+ return this.$t("settings.micron_wasm_update_source_upload");
+ }
+ return this.$t("settings.micron_wasm_update_source_github");
+ },
+ close() {
+ this.$emit("update:modelValue", false);
+ },
+ async loadCurrentInfo() {
+ try {
+ const r = await getMicronWasmRuntimeOverride();
+ if (r && r.wasmBytes) {
+ this.currentInfo = {
+ source: r.source,
+ releaseTag: r.releaseTag,
+ byteLength: r.wasmBytes.byteLength,
+ };
+ } else {
+ this.currentInfo = null;
+ }
+ } catch (e) {
+ console.warn(e);
+ this.currentInfo = null;
+ }
+ },
+ async onFetchFromGitHub() {
+ this.formError = "";
+ const tag = String(this.releaseTagInput || "").trim();
+ if (!tag) {
+ this.formError = this.$t("settings.micron_wasm_update_err_empty_tag");
+ return;
+ }
+ this.busy = true;
+ this.abort = new AbortController();
+ try {
+ const rec = await fetchWasmFromGitHubReleaseVerified(tag, { signal: this.abort.signal });
+ await setMicronWasmRuntimeOverride(rec);
+ refreshMicronWasmRuntimeOverrideCache();
+ invalidateNomadMicronWasmPreload();
+ const ok = await preloadNomadMicronWasm();
+ if (!ok) {
+ await clearMicronWasmRuntimeOverride();
+ refreshMicronWasmRuntimeOverrideCache();
+ invalidateNomadMicronWasmPreload();
+ this.formError = this.$t("settings.micron_wasm_update_err_activate_failed");
+ return;
+ }
+ ToastUtils.success(this.$t("settings.micron_wasm_update_toast_installed", { tag: rec.releaseTag }));
+ await this.loadCurrentInfo();
+ this.$emit("saved");
+ } catch (e) {
+ this.formError = (e && e.message) || String(e);
+ } finally {
+ this.busy = false;
+ this.abort = null;
+ }
+ },
+ async onWasmFileSelected(ev) {
+ this.formError = "";
+ const file = ev.target.files && ev.target.files[0];
+ if (!file) {
+ return;
+ }
+ if (!String(file.name).toLowerCase().endsWith(".wasm")) {
+ this.formError = this.$t("settings.micron_wasm_update_err_not_wasm");
+ ev.target.value = "";
+ return;
+ }
+ this.busy = true;
+ try {
+ const buf = await file.arrayBuffer();
+ if (buf.byteLength > MAX_WASM_OVERRIDE_BYTES) {
+ this.formError = this.$t("settings.micron_wasm_update_err_too_large");
+ ev.target.value = "";
+ return;
+ }
+ if (buf.byteLength < 4096) {
+ this.formError = this.$t("settings.micron_wasm_update_err_too_small");
+ ev.target.value = "";
+ return;
+ }
+ const wasmSri = await computeWasmSriSha384(buf);
+ await setMicronWasmRuntimeOverride({
+ source: "upload",
+ releaseTag: file.name,
+ wasmSri,
+ wasmBytes: buf,
+ expectedSha256Hex: null,
+ });
+ refreshMicronWasmRuntimeOverrideCache();
+ invalidateNomadMicronWasmPreload();
+ const ok = await preloadNomadMicronWasm();
+ if (!ok) {
+ await clearMicronWasmRuntimeOverride();
+ refreshMicronWasmRuntimeOverrideCache();
+ invalidateNomadMicronWasmPreload();
+ this.formError = this.$t("settings.micron_wasm_update_err_activate_failed");
+ ev.target.value = "";
+ return;
+ }
+ ToastUtils.success(this.$t("settings.micron_wasm_update_toast_uploaded"));
+ await this.loadCurrentInfo();
+ this.$emit("saved");
+ } catch (e) {
+ this.formError = (e && e.message) || String(e);
+ } finally {
+ this.busy = false;
+ ev.target.value = "";
+ }
+ },
+ async onRevertBundled() {
+ this.formError = "";
+ this.busy = true;
+ try {
+ await clearMicronWasmRuntimeOverride();
+ refreshMicronWasmRuntimeOverrideCache();
+ invalidateNomadMicronWasmPreload();
+ await preloadNomadMicronWasm();
+ ToastUtils.success(this.$t("settings.micron_wasm_update_toast_reverted"));
+ await this.loadCurrentInfo();
+ this.$emit("saved");
+ } catch (e) {
+ this.formError = (e && e.message) || String(e);
+ } finally {
+ this.busy = false;
+ }
+ },
+ },
+};
+</script>

diff --git a/meshchatx/src/frontend/components/settings/SettingsPage.vue b/meshchatx/src/frontend/components/settings/SettingsPage.vue
index a95fc7d8..4143f627 100644
--- a/meshchatx/src/frontend/components/settings/SettingsPage.vue
+++ b/meshchatx/src/frontend/components/settings/SettingsPage.vue
@@ -901,6 +901,15 @@
</span>
</span>
</label>
+ <div v-if="micronWasmBundledInBuild" class="mt-2">
+ <button
+ type="button"
+ class="primary-chip text-sm"
+ @click="micronWasmUpdateModalOpen = true"
+ >
+ {{ $t("settings.micron_wasm_update_open_btn") }}
+ </button>
+ </div>
<div class="space-y-2">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
Default page path (no URL path)
@@ -2486,6 +2495,7 @@
</div>
</div>
</div>
+ <micron-wasm-update-modal v-model="micronWasmUpdateModalOpen" @saved="onMicronWasmOverrideSaved" />
</div>
</template>
@@ -2527,6 +2537,7 @@ import {
import { normalizeRetentionValue } from "../../js/localMessageRetention";
import { matchesSettingSearch, normalizeSearchString } from "../../js/settingsSearchUtils";
import { isMicronWasmBundled } from "../../js/MicronWasmLoader.js";
+import MicronWasmUpdateModal from "./MicronWasmUpdateModal.vue";
export default {
name: "SettingsPage",
@@ -2537,6 +2548,7 @@ export default {
LxmfUserIcon,
SettingsSectionBlock,
StickerPacksManager,
+ MicronWasmUpdateModal,
},
data() {
return {
@@ -2624,6 +2636,7 @@ export default {
reloadingRns: false,
reloadRnsStatusMessage: "",
searchQuery: "",
+ micronWasmUpdateModalOpen: false,
trustedTelemetryPeers: [],
stickerCount: 0,
stickerImportReplaceDuplicates: false,
@@ -2747,13 +2760,15 @@ export default {
nomadRenderer: [
"NomadNet",
"NomadNet browser renderer",
+ "micron-parser-go",
+ "WASM",
+ "SHASUMS",
+ "micron wasm update",
"browser",
"renderer",
"markdown",
"HTML",
"plaintext",
- "WASM",
- "micron-parser-go",
"micron-parser",
"index.mu",
"index.html",
@@ -3590,6 +3605,7 @@ export default {
console.log(e);
}
},
+ onMicronWasmOverrideSaved() {},
async onNomadDefaultPagePathChange() {
await this.updateConfig(
{

diff --git a/meshchatx/src/frontend/js/MicronWasmLoader.js b/meshchatx/src/frontend/js/MicronWasmLoader.js
index 36fb9e3d..f77b8a4a 100644
--- a/meshchatx/src/frontend/js/MicronWasmLoader.js
+++ b/meshchatx/src/frontend/js/MicronWasmLoader.js
@@ -1,12 +1,36 @@
/**
* Lazy-load micron-parser-go WASM (see https://github.com/Quad4-Software/Micron-Parser-Go ).
- * Requires wasm_exec.js from Go and micron-parser-go.wasm under /vendor/micron-parser-go/.
- * Files are fetched at production build time (scripts/fetch-micron-wasm.mjs); omitted builds set VITE_MICRON_WASM_BUNDLED=false.
+ * Requires wasm_exec.js from Go and micron-parser-go.wasm under /vendor/micron-parser-go/,
+ * or a runtime WASM override from IndexedDB (see MicronWasmRuntimeOverride.js).
+ * Build-time fetch: scripts/fetch-micron-wasm.mjs; omitted builds set VITE_MICRON_WASM_BUNDLED=false.
*/
+import { getMicronWasmRuntimeOverride } from "./MicronWasmRuntimeOverride.js";
+
let resolvedPromise = null;
let integrityHashes = null;
+let resolvedOverridePromise = null;
+function readRuntimeOverrideCached() {
+ if (resolvedOverridePromise) {
+ return resolvedOverridePromise;
+ }
+ if (typeof globalThis !== "undefined" && globalThis.__MESHCHATX_TEST_MICRON_WASM_OVERRIDE__ !== undefined) {
+ const v = globalThis.__MESHCHATX_TEST_MICRON_WASM_OVERRIDE__;
+ resolvedOverridePromise = Promise.resolve(v === false ? null : v);
+ return resolvedOverridePromise;
+ }
+ resolvedOverridePromise = (async () => {
+ try {
+ return await getMicronWasmRuntimeOverride();
+ } catch (e) {
+ console.warn("Micron WASM: could not read runtime override", e);
+ return null;
+ }
+ })();
+ return resolvedOverridePromise;
+}
+
/** Computes SHA-384 hash of ArrayBuffer for SRI verification. */
async function computeSriHash(buf) {
const hash = await crypto.subtle.digest("SHA-384", buf);
@@ -59,14 +83,12 @@ async function getIntegrityHashes() {
if (integrityHashes !== null) {
return integrityHashes;
}
- // Use build-time embedded hashes as primary trusted source (tamper-proof)
const embeddedWasm = typeof __MICRON_WASM_SRI_WASM__ !== "undefined" ? __MICRON_WASM_SRI_WASM__ : "";
const embeddedExec = typeof __MICRON_WASM_SRI_EXEC__ !== "undefined" ? __MICRON_WASM_SRI_EXEC__ : "";
if (embeddedWasm && embeddedExec) {
integrityHashes = { wasm: embeddedWasm, wasmExec: embeddedExec };
return integrityHashes;
}
- // Fallback: fetch from server (less secure, but allows development builds)
try {
const res = await fetch(`${baseUrl()}/integrity.json`);
if (!res.ok) return null;
@@ -95,7 +117,6 @@ async function injectScript(src, expectedHash) {
if (document.getElementById(id)) {
return;
}
- // Fetch and verify SRI before injecting
const res = await fetch(src);
if (!res.ok) {
throw new Error(`Micron WASM: failed to fetch script ${src} (${res.status})`);
@@ -121,51 +142,102 @@ async function injectScript(src, expectedHash) {
});
}
+/**
+ * Tears down wasm_exec script tag and globals so WASM can be reloaded (e.g. after override change).
+ */
+export function teardownNomadMicronWasmRuntime() {
+ document.getElementById("meshchatx-micron-wasm-exec")?.remove();
+ try {
+ delete globalThis.micronConvert;
+ } catch {
+ globalThis.micronConvert = undefined;
+ }
+ try {
+ delete globalThis.Go;
+ } catch {
+ globalThis.Go = undefined;
+ }
+}
+
+/**
+ * Restricts the exported Go function to string/boolean inputs and string output for the Micron HTML path.
+ * Full process isolation is not available in-browser; output is still sanitized by MicronParser.
+ */
+function wrapMicronConvertForNarrowJsSurface() {
+ const inner = globalThis.micronConvert;
+ if (typeof inner !== "function" || inner.__meshchatxMicronWasmWrapped) {
+ return;
+ }
+ const wrapped = function (markup, darkTheme, forceMonospace) {
+ return inner(String(markup ?? ""), darkTheme === true, forceMonospace === true);
+ };
+ wrapped.__meshchatxMicronWasmWrapped = true;
+ globalThis.micronConvert = wrapped;
+}
+
+async function instantiateWasmBuffer(buf, go) {
+ let result;
+ try {
+ result = await WebAssembly.instantiateStreaming(
+ new Response(buf, { headers: { "content-type": "application/wasm" } }),
+ go.importObject
+ );
+ } catch {
+ result = await WebAssembly.instantiate(buf, go.importObject);
+ }
+ go.run(result.instance);
+}
+
async function instantiateOnce() {
if (typeof WebAssembly === "undefined") {
throw new Error("Micron WASM: WebAssembly is not available");
}
const root = baseUrl();
+ const override = await readRuntimeOverrideCached();
const integrity = await getIntegrityHashes();
- await injectScript(`${root}/wasm_exec.js`, integrity?.wasmExec);
+ if (!integrity?.wasmExec) {
+ throw new Error("Micron WASM: wasm_exec SRI missing (build without WASM vendor files?)");
+ }
+
+ await injectScript(`${root}/wasm_exec.js`, integrity.wasmExec);
if (typeof globalThis.Go === "undefined") {
throw new Error("Micron WASM: Go runtime missing after wasm_exec.js load");
}
- const wasmUrl = `${root}/micron-parser-go.wasm`;
const go = new globalThis.Go();
- let result;
- try {
- // Try streaming first
+
+ if (override && override.wasmBytes && override.wasmSri) {
+ await verifySri(override.wasmBytes, override.wasmSri, "micron-parser-go.wasm (runtime override)");
+ await instantiateWasmBuffer(override.wasmBytes, go);
+ } else {
+ const wasmUrl = `${root}/micron-parser-go.wasm`;
const res = await fetch(wasmUrl);
if (!res.ok) {
throw new Error(`Micron WASM: fetch failed (${res.status})`);
}
const buf = await res.arrayBuffer();
await verifySri(buf, integrity?.wasm, "micron-parser-go.wasm");
- result = await WebAssembly.instantiateStreaming(
- new Response(buf, { headers: { "content-type": "application/wasm" } }),
- go.importObject
- );
- } catch {
- // Fallback to buffer instantiation
- const buf = await fetch(wasmUrl).then((r) => {
- if (!r.ok) {
- throw new Error(`Micron WASM: fetch failed (${r.status})`);
- }
- return r.arrayBuffer();
- });
- await verifySri(buf, integrity?.wasm, "micron-parser-go.wasm");
- result = await WebAssembly.instantiate(buf, go.importObject);
+ await instantiateWasmBuffer(buf, go);
}
- go.run(result.instance);
+
if (typeof globalThis.micronConvert !== "function") {
throw new Error("Micron WASM: micronConvert was not registered");
}
+ wrapMicronConvertForNarrowJsSurface();
}
export function invalidateNomadMicronWasmPreload() {
resolvedPromise = null;
+ integrityHashes = null;
+ resolvedOverridePromise = null;
+ teardownNomadMicronWasmRuntime();
+}
+
+/**
+ * Call after installing or clearing a runtime WASM override so the next preload re-reads IndexedDB.
+ */
+export function refreshMicronWasmRuntimeOverrideCache() {
+ resolvedOverridePromise = null;
}
/**

diff --git a/meshchatx/src/frontend/js/MicronWasmRuntimeOverride.js b/meshchatx/src/frontend/js/MicronWasmRuntimeOverride.js
new file mode 100644
index 00000000..08d55fe9
--- /dev/null
+++ b/meshchatx/src/frontend/js/MicronWasmRuntimeOverride.js
@@ -0,0 +1,269 @@
+/**
+ * Optional runtime override for micron-parser-go WASM (IndexedDB).
+ * GitHub releases are verified with SHASUMS256.txt before install.
+ */
+
+const DB_NAME = "meshchatx_micron_wasm_override";
+const DB_VERSION = 1;
+const STORE = "kv";
+const KEY = "runtime_override";
+
+/** @typedef {{ source: "github"|"upload", releaseTag: string, wasmSri: string, wasmBytes: ArrayBuffer, expectedSha256Hex: string|null }} MicronWasmRuntimeOverrideRecord */
+
+export const MICRON_PARSER_GO_RELEASE_DOWNLOAD_BASE =
+ "https://github.com/Quad4-Software/Micron-Parser-Go/releases/download";
+
+export const WASM_FILENAME = "micron-parser-go.wasm";
+export const SHASUMS256_FILENAME = "SHASUMS256.txt";
+
+export const MAX_WASM_OVERRIDE_BYTES = 14 * 1024 * 1024;
+
+/**
+ * Same-origin MeshChat API path; server proxies only Quad4-Software/Micron-Parser-Go release assets (CSP).
+ * @param {string} tag
+ * @param {string} assetName SHASUMS256.txt | micron-parser-go.wasm
+ */
+export function micronParserGoReleaseProxyUrl(tag, assetName) {
+ const params = new URLSearchParams();
+ params.set("tag", tag);
+ params.set("asset", assetName);
+ return `/api/v1/tools/micron-parser-go-release?${params.toString()}`;
+}
+
+async function readFetchErrorDetail(res) {
+ const ct = res.headers.get("content-type") || "";
+ if (ct.includes("application/json")) {
+ try {
+ const j = await res.json();
+ if (j && typeof j.error === "string" && j.error) {
+ return j.error;
+ }
+ } catch {
+ /* ignore */
+ }
+ }
+ return String(res.status);
+}
+
+function openDb() {
+ return new Promise((resolve, reject) => {
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
+ req.onerror = () => reject(req.error || new Error("IndexedDB open failed"));
+ req.onupgradeneeded = () => {
+ const db = req.result;
+ if (!db.objectStoreNames.contains(STORE)) {
+ db.createObjectStore(STORE);
+ }
+ };
+ req.onsuccess = () => resolve(req.result);
+ });
+}
+
+/**
+ * Parses GNU/BSD shasum output for a single filename.
+ * @param {string} text
+ * @param {string} filename e.g. micron-parser-go.wasm
+ * @returns {string|null} lowercase hex sha256 or null if not found
+ */
+export function parseShasums256ForFilename(text, filename) {
+ if (text == null || filename == null) {
+ return null;
+ }
+ const lines = String(text).split(/\r?\n/);
+ for (const raw of lines) {
+ const line = raw.trim();
+ if (!line || line.startsWith("#")) {
+ continue;
+ }
+ const m = line.match(/^([a-fA-F0-9]{64})\s+\*?(\S+)\s*$/);
+ if (!m) {
+ continue;
+ }
+ const name = m[2].trim();
+ if (name === filename || name.endsWith(`/${filename}`)) {
+ return m[1].toLowerCase();
+ }
+ }
+ return null;
+}
+
+/** @param {ArrayBuffer} buf */
+export async function sha256HexOfBuffer(buf) {
+ const d = await crypto.subtle.digest("SHA-256", buf);
+ const bytes = new Uint8Array(d);
+ let hex = "";
+ for (let i = 0; i < bytes.length; i++) {
+ hex += bytes[i].toString(16).padStart(2, "0");
+ }
+ return hex;
+}
+
+/** @param {ArrayBuffer} buf */
+export async function computeWasmSriSha384(buf) {
+ const hash = await crypto.subtle.digest("SHA-384", buf);
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(hash)));
+ return `sha384-${base64}`;
+}
+
+/**
+ * Fetches SHASUMS256.txt and WASM from a GitHub release tag; verifies SHA-256 of WASM.
+ * Does not write to storage.
+ * @param {string} tag e.g. v1.0.5
+ * @param {{ signal?: AbortSignal }} [opts]
+ * @returns {Promise<Omit<MicronWasmRuntimeOverrideRecord, never>>}
+ */
+export async function fetchWasmFromGitHubReleaseVerified(tag, opts = {}) {
+ const { signal } = opts;
+ const t = String(tag || "").trim();
+ if (!t) {
+ throw new Error("Micron WASM update: release tag is required");
+ }
+ let sumsRes;
+ try {
+ sumsRes = await fetch(micronParserGoReleaseProxyUrl(t, SHASUMS256_FILENAME), { signal, cache: "no-store" });
+ } catch (e) {
+ const msg = e && e.name === "AbortError" ? "Request aborted" : (e && e.message) || String(e);
+ throw new Error(`Micron WASM update: could not fetch release metadata (${msg})`);
+ }
+ if (!sumsRes.ok) {
+ const detail = await readFetchErrorDetail(sumsRes);
+ throw new Error(`Micron WASM update: SHASUMS256 fetch failed (${detail}). Check the tag and your network.`);
+ }
+ let sumsText;
+ try {
+ sumsText = await sumsRes.text();
+ } catch (e) {
+ throw new Error(`Micron WASM update: could not read SHASUMS256 (${e && e.message})`);
+ }
+ const expectedHex = parseShasums256ForFilename(sumsText, WASM_FILENAME);
+ if (!expectedHex) {
+ throw new Error(`Micron WASM update: ${WASM_FILENAME} not listed in ${SHASUMS256_FILENAME} for this release.`);
+ }
+ let wasmRes;
+ try {
+ wasmRes = await fetch(micronParserGoReleaseProxyUrl(t, WASM_FILENAME), { signal, cache: "no-store" });
+ } catch (e) {
+ const msg = e && e.name === "AbortError" ? "Request aborted" : (e && e.message) || String(e);
+ throw new Error(`Micron WASM update: could not download WASM (${msg})`);
+ }
+ if (!wasmRes.ok) {
+ const detail = await readFetchErrorDetail(wasmRes);
+ throw new Error(`Micron WASM update: WASM download failed (${detail})`);
+ }
+ let buf;
+ try {
+ buf = await wasmRes.arrayBuffer();
+ } catch (e) {
+ throw new Error(`Micron WASM update: could not read WASM body (${e && e.message})`);
+ }
+ if (buf.byteLength > MAX_WASM_OVERRIDE_BYTES) {
+ throw new Error(`Micron WASM update: WASM exceeds maximum size (${MAX_WASM_OVERRIDE_BYTES} bytes).`);
+ }
+ if (buf.byteLength < 4096) {
+ throw new Error("Micron WASM update: WASM file is too small to be valid.");
+ }
+ const actualHex = await sha256HexOfBuffer(buf);
+ if (actualHex !== expectedHex) {
+ throw new Error(
+ "Micron WASM update: SHA-256 mismatch after download. Refusing to install (possible tampering or corrupt transfer)."
+ );
+ }
+ const wasmSri = await computeWasmSriSha384(buf);
+ return {
+ source: "github",
+ releaseTag: t,
+ wasmSri,
+ wasmBytes: buf,
+ expectedSha256Hex: expectedHex,
+ };
+}
+
+function assertSri(wasmSri) {
+ if (typeof wasmSri !== "string" || !/^sha384-[A-Za-z0-9+/=]+$/.test(wasmSri)) {
+ throw new Error("Micron WASM update: invalid SRI format");
+ }
+}
+
+/**
+ * @param {MicronWasmRuntimeOverrideRecord} record
+ */
+export async function setMicronWasmRuntimeOverride(record) {
+ if (!record || !record.wasmBytes) {
+ throw new Error("Micron WASM update: missing WASM data");
+ }
+ const buf = record.wasmBytes;
+ if (!(buf instanceof ArrayBuffer)) {
+ throw new Error("Micron WASM update: wasmBytes must be an ArrayBuffer");
+ }
+ if (buf.byteLength > MAX_WASM_OVERRIDE_BYTES) {
+ throw new Error(`Micron WASM update: WASM exceeds maximum size (${MAX_WASM_OVERRIDE_BYTES} bytes).`);
+ }
+ assertSri(record.wasmSri);
+ const source = record.source === "upload" ? "upload" : "github";
+ const releaseTag = String(record.releaseTag || "").trim() || (source === "github" ? "unknown" : "upload");
+ const expectedSha256Hex = record.expectedSha256Hex == null ? null : String(record.expectedSha256Hex).toLowerCase();
+ const db = await openDb();
+ try {
+ const tx = db.transaction(STORE, "readwrite");
+ const st = tx.objectStore(STORE);
+ st.put(
+ {
+ source,
+ releaseTag,
+ wasmSri: record.wasmSri,
+ wasmBytes: buf,
+ expectedSha256Hex,
+ },
+ KEY
+ );
+ await new Promise((resolve, reject) => {
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => reject(tx.error || new Error("IndexedDB write failed"));
+ tx.onabort = () => reject(tx.error || new Error("IndexedDB write aborted"));
+ });
+ } finally {
+ db.close();
+ }
+}
+
+/** @returns {Promise<MicronWasmRuntimeOverrideRecord|null>} */
+export async function getMicronWasmRuntimeOverride() {
+ const db = await openDb();
+ try {
+ const tx = db.transaction(STORE, "readonly");
+ const st = tx.objectStore(STORE);
+ const raw = await new Promise((resolve, reject) => {
+ const r = st.get(KEY);
+ r.onsuccess = () => resolve(r.result || null);
+ r.onerror = () => reject(r.error || new Error("IndexedDB read failed"));
+ });
+ if (!raw || !raw.wasmBytes || !raw.wasmSri) {
+ return null;
+ }
+ return {
+ source: raw.source === "upload" ? "upload" : "github",
+ releaseTag: String(raw.releaseTag || ""),
+ wasmSri: String(raw.wasmSri),
+ wasmBytes: raw.wasmBytes,
+ expectedSha256Hex: raw.expectedSha256Hex == null ? null : String(raw.expectedSha256Hex),
+ };
+ } finally {
+ db.close();
+ }
+}
+
+export async function clearMicronWasmRuntimeOverride() {
+ const db = await openDb();
+ try {
+ const tx = db.transaction(STORE, "readwrite");
+ const st = tx.objectStore(STORE);
+ st.delete(KEY);
+ await new Promise((resolve, reject) => {
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => reject(tx.error || new Error("IndexedDB delete failed"));
+ tx.onabort = () => reject(tx.error || new Error("IndexedDB delete aborted"));
+ });
+ } finally {
+ db.close();
+ }
+}

diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index 6842b8d6..97b26c86 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -2570,7 +2570,30 @@
"nomad_micron_wasm_title": "Micron (WASM)",
"nomad_micron_wasm_desc_before_link": "Optional WASM renderer (",
"nomad_micron_wasm_link_label": "micron-parser-go",
- "nomad_micron_wasm_desc_after_link": ") für Nomad Network-Seiten. Wenn ausgeschaltet oder nicht unterstützt, wird die Standardversion verwendet."
+ "nomad_micron_wasm_desc_after_link": ") für Nomad Network-Seiten. Wenn ausgeschaltet oder nicht unterstützt, wird die Standardversion verwendet.",
+ "micron_wasm_update_open_btn": "WASM-Engine aktualisieren…",
+ "micron_wasm_update_modal_title": "Micron-WASM-Engine",
+ "micron_wasm_update_active_label": "Aktive Überschreibung",
+ "micron_wasm_update_active_source": "Quelle: {source}",
+ "micron_wasm_update_source_github": "GitHub-Release (SHA-256 geprüft)",
+ "micron_wasm_update_source_upload": "Lokale Datei (nicht verifiziert)",
+ "micron_wasm_update_active_size": "Größe: {bytes} Bytes",
+ "micron_wasm_update_bundled_only": "Es wird das mit der App gebündelte WASM verwendet. Es wird nichts aus dem Netz geladen, bis Sie eine der Aktionen unten nutzen.",
+ "micron_wasm_update_isolation_note": "Der Browser kann WebAssembly nicht vollständig von JavaScript isolieren. Diese App stellt die Micron-Konvertierung nur über eine schmale API bereit; gerendertes HTML wird weiter bereinigt. wasm_exec.js stammt immer aus dem App-Bundle, passend zur Go-Laufzeit.",
+ "micron_wasm_update_github_heading": "Installation aus GitHub-Release",
+ "micron_wasm_update_github_url_hint": "Mit „Abrufen“ lädt der MeshChat-Server von GitHub herunter (Browser bleibt same-origin für CSP). Quellpfad:",
+ "micron_wasm_update_fetch_github": "Abrufen und installieren",
+ "micron_wasm_update_upload_heading": "Installation aus Datei",
+ "micron_wasm_update_upload_warning": "Hochgeladenes WASM wird nicht auf Echtheit geprüft. Nur Dateien verwenden, denen Sie vertrauen.",
+ "micron_wasm_update_revert_bundled": "Überschreibung entfernen (gebündeltes WASM)",
+ "micron_wasm_update_err_empty_tag": "Release-Tag eingeben (z. B. v1.0.5).",
+ "micron_wasm_update_err_not_wasm": "Wählen Sie eine Datei, deren Name auf .wasm endet.",
+ "micron_wasm_update_err_too_large": "Die Datei ist zu groß für eine Laufzeit-Überschreibung.",
+ "micron_wasm_update_err_too_small": "Die Datei ist zu klein für ein gültiges WASM-Modul.",
+ "micron_wasm_update_err_activate_failed": "Installierte Datei konnte nicht geladen werden (wasm_exec-Kompatibilität prüfen oder anderen Build versuchen). Überschreibung wurde entfernt.",
+ "micron_wasm_update_toast_installed": "Micron-WASM {tag} installiert.",
+ "micron_wasm_update_toast_uploaded": "WASM aus Ihrer Datei installiert.",
+ "micron_wasm_update_toast_reverted": "Gebündeltes Micron-WASM wiederhergestellt."
},
"debug": {
"title": "Debug Logs",

diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index 1799d7e0..261d1d6f 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -1348,7 +1348,30 @@
"nomad_micron_wasm_title": "Micron (WASM)",
"nomad_micron_wasm_desc_before_link": "Use a faster engine (",
"nomad_micron_wasm_link_label": "micron-parser-go",
- "nomad_micron_wasm_desc_after_link": ") for Nomad Network pages. If turned off or unsupported, it falls back to the standard version."
+ "nomad_micron_wasm_desc_after_link": ") for Nomad Network pages. If turned off or unsupported, it falls back to the standard version.",
+ "micron_wasm_update_open_btn": "Update WASM engine…",
+ "micron_wasm_update_modal_title": "Micron WASM engine",
+ "micron_wasm_update_active_label": "Active override",
+ "micron_wasm_update_active_source": "Source: {source}",
+ "micron_wasm_update_source_github": "GitHub release (SHA-256 verified)",
+ "micron_wasm_update_source_upload": "Local file (not verified)",
+ "micron_wasm_update_active_size": "Size: {bytes} bytes",
+ "micron_wasm_update_bundled_only": "Using the WASM bundled with this app. Nothing is fetched from the network until you use the actions below.",
+ "micron_wasm_update_isolation_note": "The browser cannot fully isolate WebAssembly from JavaScript. This app only exposes Micron conversion through a narrow API; rendered HTML is still sanitized. wasm_exec.js always comes from the app bundle to match the Go runtime.",
+ "micron_wasm_update_github_heading": "Install from GitHub release",
+ "micron_wasm_update_github_url_hint": "Press Fetch so the MeshChat server downloads from GitHub (browser stays same-origin for CSP). Source path:",
+ "micron_wasm_update_fetch_github": "Fetch and install",
+ "micron_wasm_update_upload_heading": "Install from file",
+ "micron_wasm_update_upload_warning": "Uploaded WASM is not authenticity-checked. Only use files you trust.",
+ "micron_wasm_update_revert_bundled": "Remove override (use bundled WASM)",
+ "micron_wasm_update_err_empty_tag": "Enter a release tag (for example v1.0.5).",
+ "micron_wasm_update_err_not_wasm": "Choose a file whose name ends with .wasm.",
+ "micron_wasm_update_err_too_large": "That file is too large to store as a runtime override.",
+ "micron_wasm_update_err_too_small": "That file is too small to be a valid WASM module.",
+ "micron_wasm_update_err_activate_failed": "Installed file could not be loaded (check wasm_exec compatibility or try another build). Override was removed.",
+ "micron_wasm_update_toast_installed": "Installed Micron WASM {tag}.",
+ "micron_wasm_update_toast_uploaded": "Installed WASM from your file.",
+ "micron_wasm_update_toast_reverted": "Restored bundled Micron WASM."
},
"debug": {
"title": "Debug Logs",

diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index 0b7e23cf..74a64738 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -1348,7 +1348,30 @@
"nomad_micron_wasm_title": "Micron (WASM)",
"nomad_micron_wasm_desc_before_link": "Optional WASM renderer (",
"nomad_micron_wasm_link_label": "micron-parser-go",
- "nomad_micron_wasm_desc_after_link": "): faster when supported; falls back to bundled JS. Off keeps JS only."
+ "nomad_micron_wasm_desc_after_link": "): faster when supported; falls back to bundled JS. Off keeps JS only.",
+ "micron_wasm_update_open_btn": "Actualizar motor WASM…",
+ "micron_wasm_update_modal_title": "Motor Micron WASM",
+ "micron_wasm_update_active_label": "Anulación activa",
+ "micron_wasm_update_active_source": "Origen: {source}",
+ "micron_wasm_update_source_github": "Versión en GitHub (SHA-256 verificado)",
+ "micron_wasm_update_source_upload": "Archivo local (sin verificar)",
+ "micron_wasm_update_active_size": "Tamaño: {bytes} bytes",
+ "micron_wasm_update_bundled_only": "Se usa el WASM incluido con la aplicación. No se descarga nada de la red hasta que use las acciones siguientes.",
+ "micron_wasm_update_isolation_note": "El navegador no puede aislar por completo WebAssembly de JavaScript. Esta aplicación solo expone la conversión Micron mediante una API limitada; el HTML generado sigue saneándose. wasm_exec.js siempre proviene del paquete de la aplicación para coincidir con el runtime de Go.",
+ "micron_wasm_update_github_heading": "Instalar desde versión de GitHub",
+ "micron_wasm_update_github_url_hint": "Pulse Obtener para que el servidor MeshChat descargue desde GitHub (el navegador sigue en el mismo origen por la CSP). Ruta de origen:",
+ "micron_wasm_update_fetch_github": "Obtener e instalar",
+ "micron_wasm_update_upload_heading": "Instalar desde archivo",
+ "micron_wasm_update_upload_warning": "El WASM subido no se comprueba por autenticidad. Use solo archivos en los que confíe.",
+ "micron_wasm_update_revert_bundled": "Quitar anulación (usar WASM incluido)",
+ "micron_wasm_update_err_empty_tag": "Introduzca una etiqueta de versión (por ejemplo v1.0.5).",
+ "micron_wasm_update_err_not_wasm": "Elija un archivo cuyo nombre termine en .wasm.",
+ "micron_wasm_update_err_too_large": "El archivo es demasiado grande para guardarlo como anulación en tiempo de ejecución.",
+ "micron_wasm_update_err_too_small": "El archivo es demasiado pequeño para ser un módulo WASM válido.",
+ "micron_wasm_update_err_activate_failed": "No se pudo cargar el archivo instalado (compruebe compatibilidad con wasm_exec o pruebe otra compilación). Se eliminó la anulación.",
+ "micron_wasm_update_toast_installed": "Micron WASM {tag} instalado.",
+ "micron_wasm_update_toast_uploaded": "WASM instalado desde su archivo.",
+ "micron_wasm_update_toast_reverted": "Restaurado el WASM Micron incluido."
},
"debug": {
"title": "Debug Logs",

diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index ca74f515..5ac7add5 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -1348,7 +1348,30 @@
"nomad_micron_wasm_title": "Micron (WASM)",
"nomad_micron_wasm_desc_before_link": "Optional WASM renderer (",
"nomad_micron_wasm_link_label": "micron-parser-go",
- "nomad_micron_wasm_desc_after_link": "): faster when supported; falls back to bundled JS. Off keeps JS only."
+ "nomad_micron_wasm_desc_after_link": "): faster when supported; falls back to bundled JS. Off keeps JS only.",
+ "micron_wasm_update_open_btn": "Mettre à jour le moteur WASM…",
+ "micron_wasm_update_modal_title": "Moteur Micron WASM",
+ "micron_wasm_update_active_label": "Remplacement actif",
+ "micron_wasm_update_active_source": "Source : {source}",
+ "micron_wasm_update_source_github": "Version GitHub (SHA-256 vérifié)",
+ "micron_wasm_update_source_upload": "Fichier local (non vérifié)",
+ "micron_wasm_update_active_size": "Taille : {bytes} octets",
+ "micron_wasm_update_bundled_only": "Utilisation du WASM fourni avec l’application. Aucun téléchargement réseau tant que vous n’utilisez pas les actions ci-dessous.",
+ "micron_wasm_update_isolation_note": "Le navigateur ne peut pas isoler totalement WebAssembly de JavaScript. Cette application n’expose la conversion Micron que via une API restreinte ; le HTML produit reste assaini. wasm_exec.js provient toujours du bundle pour correspondre au runtime Go.",
+ "micron_wasm_update_github_heading": "Installer depuis une version GitHub",
+ "micron_wasm_update_github_url_hint": "Appuyez sur Récupérer pour que le serveur MeshChat télécharge depuis GitHub (navigateur same-origin pour la CSP). Chemin source :",
+ "micron_wasm_update_fetch_github": "Récupérer et installer",
+ "micron_wasm_update_upload_heading": "Installer depuis un fichier",
+ "micron_wasm_update_upload_warning": "Le WASM téléversé n’est pas contrôlé pour l’authenticité. N’utilisez que des fichiers de confiance.",
+ "micron_wasm_update_revert_bundled": "Supprimer le remplacement (WASM fourni)",
+ "micron_wasm_update_err_empty_tag": "Saisissez une étiquette de version (par ex. v1.0.5).",
+ "micron_wasm_update_err_not_wasm": "Choisissez un fichier dont le nom se termine par .wasm.",
+ "micron_wasm_update_err_too_large": "Ce fichier est trop volumineux pour un remplacement d’exécution.",
+ "micron_wasm_update_err_too_small": "Ce fichier est trop petit pour être un module WASM valide.",
+ "micron_wasm_update_err_activate_failed": "Impossible de charger le fichier installé (vérifiez la compatibilité wasm_exec ou essayez une autre build). Le remplacement a été supprimé.",
+ "micron_wasm_update_toast_installed": "Micron WASM {tag} installé.",
+ "micron_wasm_update_toast_uploaded": "WASM installé depuis votre fichier.",
+ "micron_wasm_update_toast_reverted": "WASM Micron fourni avec l’application rétabli."
},
"debug": {
"title": "Débogues",

diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index 56a4d2a8..1b2bdfed 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -1400,7 +1400,30 @@
"nomad_micron_wasm_title": "Micron (WASM)",
"nomad_micron_wasm_desc_before_link": "Optional WASM renderer (",
"nomad_micron_wasm_link_label": "micron-parser-go",
- "nomad_micron_wasm_desc_after_link": ") per le pagine Nomad Network. Se disattivata o non supportata, viene usata la versione standard."
+ "nomad_micron_wasm_desc_after_link": ") per le pagine Nomad Network. Se disattivata o non supportata, viene usata la versione standard.",
+ "micron_wasm_update_open_btn": "Aggiorna motore WASM…",
+ "micron_wasm_update_modal_title": "Motore Micron WASM",
+ "micron_wasm_update_active_label": "Sostituzione attiva",
+ "micron_wasm_update_active_source": "Origine: {source}",
+ "micron_wasm_update_source_github": "Release GitHub (SHA-256 verificato)",
+ "micron_wasm_update_source_upload": "File locale (non verificato)",
+ "micron_wasm_update_active_size": "Dimensione: {bytes} byte",
+ "micron_wasm_update_bundled_only": "Si usa il WASM incluso nell’app. Nulla viene scaricato dalla rete finché non usi le azioni sotto.",
+ "micron_wasm_update_isolation_note": "Il browser non può isolare completamente WebAssembly da JavaScript. Questa app espone la conversione Micron solo tramite un’API ristretta; l’HTML generato resta sanificato. wasm_exec.js proviene sempre dal bundle dell’app per allinearsi al runtime Go.",
+ "micron_wasm_update_github_heading": "Installa da release GitHub",
+ "micron_wasm_update_github_url_hint": "Premi Scarica: il server MeshChat scarica da GitHub (il browser resta same-origin per la CSP). Percorso sorgente:",
+ "micron_wasm_update_fetch_github": "Scarica e installa",
+ "micron_wasm_update_upload_heading": "Installa da file",
+ "micron_wasm_update_upload_warning": "Il WASM caricato non è controllato per autenticità. Usa solo file di cui ti fidi.",
+ "micron_wasm_update_revert_bundled": "Rimuovi sostituzione (usa WASM incluso)",
+ "micron_wasm_update_err_empty_tag": "Inserisci un tag di release (ad es. v1.0.5).",
+ "micron_wasm_update_err_not_wasm": "Scegli un file il cui nome termina con .wasm.",
+ "micron_wasm_update_err_too_large": "Il file è troppo grande per essere salvato come sostituzione a runtime.",
+ "micron_wasm_update_err_too_small": "Il file è troppo piccolo per essere un modulo WASM valido.",
+ "micron_wasm_update_err_activate_failed": "Impossibile caricare il file installato (controlla compatibilità wasm_exec o prova un’altra build). Sostituzione rimossa.",
+ "micron_wasm_update_toast_installed": "Micron WASM {tag} installato.",
+ "micron_wasm_update_toast_uploaded": "WASM installato dal tuo file.",
+ "micron_wasm_update_toast_reverted": "Ripristinato il WASM Micron incluso nell’app."
},
"debug": {
"title": "Log di Debug",

diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index 7fbf6728..c6e70b8d 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -1348,7 +1348,30 @@
"nomad_micron_wasm_title": "Micron (WASM)",
"nomad_micron_wasm_desc_before_link": "Optional WASM renderer (",
"nomad_micron_wasm_link_label": "micron-parser-go",
- "nomad_micron_wasm_desc_after_link": ") voor Nomad Network-pagina's. Als dit is uitgeschakeld of niet wordt ondersteund, wordt de standaardversie gebruikt."
+ "nomad_micron_wasm_desc_after_link": ") voor Nomad Network-pagina's. Als dit is uitgeschakeld of niet wordt ondersteund, wordt de standaardversie gebruikt.",
+ "micron_wasm_update_open_btn": "WASM-engine bijwerken…",
+ "micron_wasm_update_modal_title": "Micron WASM-engine",
+ "micron_wasm_update_active_label": "Actieve overschrijving",
+ "micron_wasm_update_active_source": "Bron: {source}",
+ "micron_wasm_update_source_github": "GitHub-release (SHA-256 gecontroleerd)",
+ "micron_wasm_update_source_upload": "Lokaal bestand (niet geverifieerd)",
+ "micron_wasm_update_active_size": "Grootte: {bytes} bytes",
+ "micron_wasm_update_bundled_only": "De met de app gebundelde WASM wordt gebruikt. Er wordt niets van het netwerk gehaald tot u onderstaande acties gebruikt.",
+ "micron_wasm_update_isolation_note": "De browser kan WebAssembly niet volledig van JavaScript isoleren. Deze app biedt Micron-conversie alleen via een beperkte API; gerenderde HTML wordt nog steeds geschoond. wasm_exec.js komt altijd uit het app-bundel om bij de Go-runtime te passen.",
+ "micron_wasm_update_github_heading": "Installeren vanaf GitHub-release",
+ "micron_wasm_update_github_url_hint": "Druk op Ophalen zodat de MeshChat-server van GitHub downloadt (browser blijft same-origin voor CSP). Bronpad:",
+ "micron_wasm_update_fetch_github": "Ophalen en installeren",
+ "micron_wasm_update_upload_heading": "Installeren vanaf bestand",
+ "micron_wasm_update_upload_warning": "Geüploade WASM wordt niet op echtheid gecontroleerd. Gebruik alleen bestanden die u vertrouwt.",
+ "micron_wasm_update_revert_bundled": "Overschrijving verwijderen (gebundelde WASM)",
+ "micron_wasm_update_err_empty_tag": "Voer een releasetag in (bijv. v1.0.5).",
+ "micron_wasm_update_err_not_wasm": "Kies een bestand waarvan de naam eindigt op .wasm.",
+ "micron_wasm_update_err_too_large": "Dit bestand is te groot om als runtime-overschrijving op te slaan.",
+ "micron_wasm_update_err_too_small": "Dit bestand is te klein om een geldige WASM-module te zijn.",
+ "micron_wasm_update_err_activate_failed": "Geïnstalleerd bestand kon niet worden geladen (controleer wasm_exec-compatibiliteit of probeer een andere build). Overschrijving is verwijderd.",
+ "micron_wasm_update_toast_installed": "Micron WASM {tag} geïnstalleerd.",
+ "micron_wasm_update_toast_uploaded": "WASM geïnstalleerd vanaf uw bestand.",
+ "micron_wasm_update_toast_reverted": "Gebundelde Micron WASM hersteld."
},
"debug": {
"title": "Debuglogs",

diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index 378cf64d..4e4c7d07 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -2570,7 +2570,30 @@
"nomad_micron_wasm_title": "Micron (WASM)",
"nomad_micron_wasm_desc_before_link": "Optional WASM renderer (",
"nomad_micron_wasm_link_label": "micron-parser-go",
- "nomad_micron_wasm_desc_after_link": ") для страниц Nomad Network. Если выключено или не поддерживается, используется стандартная версия."
+ "nomad_micron_wasm_desc_after_link": ") для страниц Nomad Network. Если выключено или не поддерживается, используется стандартная версия.",
+ "micron_wasm_update_open_btn": "Обновить движок WASM…",
+ "micron_wasm_update_modal_title": "Движок Micron WASM",
+ "micron_wasm_update_active_label": "Активное переопределение",
+ "micron_wasm_update_active_source": "Источник: {source}",
+ "micron_wasm_update_source_github": "Релиз GitHub (проверка SHA-256)",
+ "micron_wasm_update_source_upload": "Локальный файл (без проверки подлинности)",
+ "micron_wasm_update_active_size": "Размер: {bytes} байт",
+ "micron_wasm_update_bundled_only": "Используется WASM, поставляемый с приложением. Сеть не запрашивается, пока вы не выполните действия ниже.",
+ "micron_wasm_update_isolation_note": "Браузер не может полностью изолировать WebAssembly от JavaScript. Приложение выставляет преобразование Micron только через узкий API; HTML по-прежнему очищается. wasm_exec.js всегда берётся из пакета приложения для соответствия среде Go.",
+ "micron_wasm_update_github_heading": "Установка из релиза GitHub",
+ "micron_wasm_update_github_url_hint": "Нажмите «Загрузить», чтобы сервер MeshChat скачал с GitHub (браузер остаётся same-origin для CSP). Путь источника:",
+ "micron_wasm_update_fetch_github": "Загрузить и установить",
+ "micron_wasm_update_upload_heading": "Установка из файла",
+ "micron_wasm_update_upload_warning": "Загруженный WASM не проверяется на подлинность. Используйте только доверенные файлы.",
+ "micron_wasm_update_revert_bundled": "Убрать переопределение (встроенный WASM)",
+ "micron_wasm_update_err_empty_tag": "Введите тег релиза (например v1.0.5).",
+ "micron_wasm_update_err_not_wasm": "Выберите файл с расширением .wasm.",
+ "micron_wasm_update_err_too_large": "Файл слишком большой для сохранения как переопределение.",
+ "micron_wasm_update_err_too_small": "Файл слишком мал для корректного WASM-модуля.",
+ "micron_wasm_update_err_activate_failed": "Не удалось загрузить установленный файл (проверьте совместимость wasm_exec или другую сборку). Переопределение удалено.",
+ "micron_wasm_update_toast_installed": "Установлен Micron WASM {tag}.",
+ "micron_wasm_update_toast_uploaded": "WASM установлен из вашего файла.",
+ "micron_wasm_update_toast_reverted": "Восстановлен встроенный Micron WASM."
},
"debug": {
"title": "Журнал отладки",

diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index af2e54c9..219fb3e4 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -1348,7 +1348,30 @@
"nomad_micron_wasm_title": "Micron (WASM)",
"nomad_micron_wasm_desc_before_link": "Optional WASM renderer (",
"nomad_micron_wasm_link_label": "micron-parser-go",
- "nomad_micron_wasm_desc_after_link": "): faster when supported; falls back to bundled JS. Off keeps JS only."
+ "nomad_micron_wasm_desc_after_link": "): faster when supported; falls back to bundled JS. Off keeps JS only.",
+ "micron_wasm_update_open_btn": "更新 WASM 引擎…",
+ "micron_wasm_update_modal_title": "Micron WASM 引擎",
+ "micron_wasm_update_active_label": "当前覆盖",
+ "micron_wasm_update_active_source": "来源:{source}",
+ "micron_wasm_update_source_github": "GitHub 发行版(已验证 SHA-256)",
+ "micron_wasm_update_source_upload": "本地文件(未验证真实性)",
+ "micron_wasm_update_active_size": "大小:{bytes} 字节",
+ "micron_wasm_update_bundled_only": "正在使用与应用打包的 WASM。在您执行下方操作前不会从网络获取任何内容。",
+ "micron_wasm_update_isolation_note": "浏览器无法将 WebAssembly 与 JavaScript 完全隔离。本应用仅通过受限 API 暴露 Micron 转换;渲染的 HTML 仍会经过净化。wasm_exec.js 始终来自应用包,以匹配 Go 运行时。",
+ "micron_wasm_update_github_heading": "从 GitHub 发行版安装",
+ "micron_wasm_update_github_url_hint": "点击“获取并安装”后由 MeshChat 服务器从 GitHub 下载(浏览器保持同源以满足 CSP)。源路径:",
+ "micron_wasm_update_fetch_github": "获取并安装",
+ "micron_wasm_update_upload_heading": "从文件安装",
+ "micron_wasm_update_upload_warning": "上传的 WASM 不会进行真实性校验。请仅使用您信任的文件。",
+ "micron_wasm_update_revert_bundled": "移除覆盖(使用打包的 WASM)",
+ "micron_wasm_update_err_empty_tag": "请输入发行标签(例如 v1.0.5)。",
+ "micron_wasm_update_err_not_wasm": "请选择扩展名为 .wasm 的文件。",
+ "micron_wasm_update_err_too_large": "文件过大,无法作为运行时覆盖保存。",
+ "micron_wasm_update_err_too_small": "文件过小,不是有效的 WASM 模块。",
+ "micron_wasm_update_err_activate_failed": "无法加载已安装的文件(请检查 wasm_exec 兼容性或尝试其他构建)。已移除覆盖。",
+ "micron_wasm_update_toast_installed": "已安装 Micron WASM {tag}。",
+ "micron_wasm_update_toast_uploaded": "已从您的文件安装 WASM。",
+ "micron_wasm_update_toast_reverted": "已恢复为打包的 Micron WASM。"
},
"debug": {
"title": "调试日志",

diff --git a/meshchatx/src/frontend/public/vendor/micron-parser-go/integrity.json b/meshchatx/src/frontend/public/vendor/micron-parser-go/integrity.json
index 54fa21a4..e44261ca 100644
--- a/meshchatx/src/frontend/public/vendor/micron-parser-go/integrity.json
+++ b/meshchatx/src/frontend/public/vendor/micron-parser-go/integrity.json
@@ -1,5 +1,5 @@
{
- "version": "v1.0.4",
- "wasm": "sha384-yfOVcfwRqbegeAnDNdC6z83Z7eGPBKJ78L2iNNrqSzhVfl/uYomk2KfvA3t2JyCS",
+ "version": "v1.0.5",
+ "wasm": "sha384-E16yCcmQKfjrg2OxZQ2uY8ZnQoJvQK9y7NmH4BJOO8qO5Eg677HPie1AbZqWKfcK",
"wasmExec": "sha384-PWCs+V4BDf9yY1yjkD/p+9xNEs4iEbuvq+HezAOJiY3XL5GI6VyJXMsvnjiwNbce"
}
\ No newline at end of file

diff --git a/scripts/micron-parser-go-version.mjs b/scripts/micron-parser-go-version.mjs
index 4169bdf4..ade51a88 100644
--- a/scripts/micron-parser-go-version.mjs
+++ b/scripts/micron-parser-go-version.mjs
@@ -2,4 +2,4 @@
* Micron-Parser-Go release tag for default WASM URL (scripts/fetch-micron-wasm.mjs)
* and build-time tooltip copy (VITE_MICRON_PARSER_GO_RELEASE).
*/
-export const MICRON_PARSER_GO_RELEASE_TAG = "v1.0.4";
+export const MICRON_PARSER_GO_RELEASE_TAG = "v1.0.5";

diff --git a/tests/frontend/MicronWasmLoader.test.js b/tests/frontend/MicronWasmLoader.test.js
index 04be77ff..0c05436a 100644
--- a/tests/frontend/MicronWasmLoader.test.js
+++ b/tests/frontend/MicronWasmLoader.test.js
@@ -232,6 +232,12 @@ describe("MicronWasmLoader.js", () => {
try {
expect(await preloadNomadMicronWasm()).toBe(false);
invalidateNomadMicronWasmPreload();
+ globalThis.Go = class {
+ constructor() {
+ this.importObject = {};
+ this.run = vi.fn();
+ }
+ };
expect(await preloadNomadMicronWasm()).toBe(false);
expect(instantiate).toHaveBeenCalledTimes(2);
} finally {

diff --git a/tests/frontend/MicronWasmRuntimeOverride.test.js b/tests/frontend/MicronWasmRuntimeOverride.test.js
new file mode 100644
index 00000000..be5afc59
--- /dev/null
+++ b/tests/frontend/MicronWasmRuntimeOverride.test.js
@@ -0,0 +1,131 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import {
+ WASM_FILENAME,
+ clearMicronWasmRuntimeOverride,
+ computeWasmSriSha384,
+ fetchWasmFromGitHubReleaseVerified,
+ getMicronWasmRuntimeOverride,
+ parseShasums256ForFilename,
+ setMicronWasmRuntimeOverride,
+ sha256HexOfBuffer,
+} from "../../meshchatx/src/frontend/js/MicronWasmRuntimeOverride.js";
+import {
+ invalidateNomadMicronWasmPreload,
+ refreshMicronWasmRuntimeOverrideCache,
+} from "../../meshchatx/src/frontend/js/MicronWasmLoader.js";
+
+describe("MicronWasmRuntimeOverride.js", () => {
+ beforeEach(async () => {
+ await clearMicronWasmRuntimeOverride();
+ refreshMicronWasmRuntimeOverrideCache();
+ invalidateNomadMicronWasmPreload();
+ });
+
+ afterEach(async () => {
+ await clearMicronWasmRuntimeOverride();
+ refreshMicronWasmRuntimeOverrideCache();
+ invalidateNomadMicronWasmPreload();
+ vi.restoreAllMocks();
+ });
+
+ it("parseShasums256ForFilename reads hex line", () => {
+ const text = "c2b0f7de7f241719b54f99b12dc8215e262bdbecb8a5b2a25de3848408aa44cb micron-parser-go.wasm\n";
+ expect(parseShasums256ForFilename(text, WASM_FILENAME)).toBe(
+ "c2b0f7de7f241719b54f99b12dc8215e262bdbecb8a5b2a25de3848408aa44cb"
+ );
+ });
+
+ it("parseShasums256ForFilename supports BSD asterisk prefix", () => {
+ const text = "aa".repeat(32) + " *micron-parser-go.wasm\n";
+ expect(parseShasums256ForFilename(text, WASM_FILENAME)).toBe("aa".repeat(32));
+ });
+
+ it("parseShasums256ForFilename returns null when filename missing", () => {
+ expect(parseShasums256ForFilename("abcd".repeat(16) + " other.bin\n", WASM_FILENAME)).toBe(null);
+ });
+
+ it("setMicronWasmRuntimeOverride round-trips via IndexedDB", async () => {
+ const wasmBytes = new Uint8Array(5000).fill(7).buffer;
+ const wasmSri = await computeWasmSriSha384(wasmBytes);
+ await setMicronWasmRuntimeOverride({
+ source: "upload",
+ releaseTag: "test.wasm",
+ wasmSri,
+ wasmBytes,
+ expectedSha256Hex: null,
+ });
+ const got = await getMicronWasmRuntimeOverride();
+ expect(got).not.toBeNull();
+ expect(got.source).toBe("upload");
+ expect(got.releaseTag).toBe("test.wasm");
+ expect(got.wasmSri).toBe(wasmSri);
+ expect(got.wasmBytes.byteLength).toBe(5000);
+ });
+
+ it("fetchWasmFromGitHubReleaseVerified rejects hash mismatch", async () => {
+ const wasmBytes = new Uint8Array(5000).fill(9).buffer;
+ const expectedHex = await sha256HexOfBuffer(wasmBytes);
+ const wrongHex = "0".repeat(64);
+ const sums = `${wrongHex} ${WASM_FILENAME}\n`;
+ vi.stubGlobal(
+ "fetch",
+ vi.fn((url) => {
+ const u = String(url);
+ if (u.includes("micron-parser-go-release") && u.includes("asset=SHASUMS256.txt")) {
+ return Promise.resolve(new Response(sums, { status: 200 }));
+ }
+ if (u.includes("micron-parser-go-release") && u.includes("asset=micron-parser-go.wasm")) {
+ return Promise.resolve(new Response(wasmBytes, { status: 200 }));
+ }
+ return Promise.resolve(new Response("", { status: 404 }));
+ })
+ );
+ await expect(fetchWasmFromGitHubReleaseVerified("v9.9.9")).rejects.toThrow(/SHA-256 mismatch/);
+ });
+
+ it("fetchWasmFromGitHubReleaseVerified succeeds when SHASUMS matches WASM", async () => {
+ const wasmBytes = new Uint8Array(5000).fill(3).buffer;
+ const expectedHex = await sha256HexOfBuffer(wasmBytes);
+ const sums = `${expectedHex} ${WASM_FILENAME}\n`;
+ vi.stubGlobal(
+ "fetch",
+ vi.fn((url) => {
+ const u = String(url);
+ if (u.includes("micron-parser-go-release") && u.includes("asset=SHASUMS256.txt")) {
+ return Promise.resolve(new Response(sums, { status: 200 }));
+ }
+ if (u.includes("micron-parser-go-release") && u.includes("asset=micron-parser-go.wasm")) {
+ return Promise.resolve(new Response(wasmBytes, { status: 200 }));
+ }
+ return Promise.resolve(new Response("", { status: 404 }));
+ })
+ );
+ const rec = await fetchWasmFromGitHubReleaseVerified("v1.0.0");
+ expect(rec.source).toBe("github");
+ expect(rec.releaseTag).toBe("v1.0.0");
+ expect(rec.expectedSha256Hex).toBe(expectedHex);
+ expect(rec.wasmBytes.byteLength).toBe(5000);
+ expect(rec.wasmSri).toMatch(/^sha384-/);
+ });
+
+ it("fetchWasmFromGitHubReleaseVerified maps network failure to readable error", async () => {
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(() => Promise.reject(new TypeError("Failed to fetch")))
+ );
+ await expect(fetchWasmFromGitHubReleaseVerified("v1.0.0")).rejects.toThrow(/could not fetch release metadata/i);
+ });
+
+ it("fetchWasmFromGitHubReleaseVerified handles missing SHASUMS line", async () => {
+ vi.stubGlobal(
+ "fetch",
+ vi.fn((url) => {
+ if (String(url).includes("asset=SHASUMS256.txt")) {
+ return Promise.resolve(new Response("deadbeef\n", { status: 200 }));
+ }
+ return Promise.resolve(new Response("", { status: 404 }));
+ })
+ );
+ await expect(fetchWasmFromGitHubReleaseVerified("v1.0.0")).rejects.toThrow(/not listed/);
+ });
+});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────